﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Red.GameData.Raw
{
    /// <summary>
    /// A collection of all object data loaded from files
    /// </summary>
    partial class DataCollection
    {
        /// <summary>
        /// Create totally new __named__ object, if no file is suggested object lands in the unassigned list.
        /// Class name is required - without it we are not able to create the object
        /// Class name is NOT verified
        /// </summary>
        /// <param name="suggestedFile"></param>
        /// <param name="className"></param>
        /// <returns></returns>
        public DataObject Action_NewRootObject(ReflectionData reflection, DataFile suggestedFile, string className, string objectName)
        {
            // no class specified
            if (className == "")
                return null;

            // setup default properties - get the actual reflection class
            var objClass = reflection.FindClass( className );
            if ( objClass == null )
                return null;

            // unable to create nameless root objects
            var keyName = objClass.KeyName;
            if ( keyName == "")
                return null;

            // named object must have a name specified
            if ( objectName == "" )
                return null;

            // target file
            var file = (suggestedFile == null) ? _unassignedObjects : suggestedFile;

            // create the object
            DataObject obj = new DataObject(file, Guid.NewGuid(), Guid.Empty /* no base */, Guid.Empty /* no parent */, className);
            file.ObjectsInFile.Add(obj);

            // set object name
            obj.GetProperty(keyName).Action_ModifyValue(objectName);

            // register in global lists
            AllObjects.Add(obj);
            _objectMap[obj.ID] = obj;

            // new objects are considered dirty by default
            obj.MarkModified();
            return obj;
        }

        /// <summary>
        /// A little bit hacky functions used to generate automatic names for newly created objects
        /// </summary>
        /// <returns></returns>
        private string UniquePostfixGenerator()
        {
            var bytes = Guid.NewGuid().ToByteArray();
            return BitConverter.ToUInt32(bytes, 4).ToString("X8");
        }

        // create totally new named object VIEW
        public DataObjectView Action_NewRootObjectView(ReflectionData reflection, DataFile suggestedFile, string className, string objectName)
        {
            // create random name
            if (objectName == "")
                objectName = className + UniquePostfixGenerator();

            // create the object
            DataObject newRawObj = Action_NewRootObject(reflection, suggestedFile, className, objectName);
            if (newRawObj == null)
                return null;

            // create wrapping view
            var objClass = reflection.FindClass(newRawObj.ClassName);
            return new DataObjectView(className, objClass.KeyName, newRawObj);
        }

        /// <summary>
        /// Create totally new __named__ object, if no file is suggested object lands in the unassigned list.
        /// Class name is required - without it we are not able to create the object
        /// Class name is NOT verified
        /// </summary>
        /// <param name="suggestedFile"></param>
        /// <param name="className"></param>
        /// <param name="guid"></param>
        /// <returns></returns>
        public DataObject Action_NewUnnamedObject(ReflectionData reflection, DataObject parentObject, string className, Guid guid = default( Guid ) )
        {
            // no class specified
            if (className == "")
                return null;

            // setup default properties - get the actual reflection class
            var objClass = reflection.FindClass(className);
            if (objClass == null)
                return null;

            // target file
            var file = parentObject.File;
            var parentId = parentObject.ID;

            if ( guid == Guid.Empty )
                guid = Guid.NewGuid();

            // create the object
            DataObject obj = new DataObject( file, guid, Guid.Empty /* no base */, parentId, className );
            file.ObjectsInFile.Add(obj);

            // register in global lists
            AllObjects.Add(obj);
            _objectMap[obj.ID] = obj;

            // new objects are considered dirty by default
            obj.MarkModified();
            return obj;
        }

        // derive from existing object, the new object is configured to point at the source object as base and to be in the same file
        public DataObject Action_DeriveObject(DataObject parentObject, DataObject baseObject, string classOverride = "")
        {
            // we can't derive from nothing
            if (baseObject == null)
                return null;

            // create new object exactly at the same file as the base
            // no class change
            var parentId = (parentObject != null) ? parentObject.ID : Guid.Empty;
            DataObject obj = new DataObject(baseObject.File, Guid.NewGuid(), baseObject.ID, parentId, classOverride);
            baseObject.File.ObjectsInFile.Add(obj);

            // register in global maps
            AllObjects.Add(obj);
            _objectMap[obj.ID] = obj;

            // new objects are considered dirty by default
            obj.MarkModified();
            return obj;
        }

        // derive from existing object VIEW, the new object is configured to point at the source object as base and to be in the same file
        public DataObjectView Action_DeriveObject(DataObjectView parentObjectView, DataObjectView baseObjectView, string classOverride = "")
        {
            // we can't derive from nothing
            if (baseObjectView == null)
                return null;

            // create the object
            var parentObject = (parentObjectView != null) ? parentObjectView.RawData : null;
            var newRawObj = Action_DeriveObject(parentObject, baseObjectView.RawData, classOverride);

            // setup new name
            var drivedName = baseObjectView.Name + "Sub_" + UniquePostfixGenerator();
            newRawObj.GetProperty(baseObjectView.KeyProperty).Action_ModifyValue(drivedName); // fancy way to set the name of the object

            // setup view
            var view = new DataObjectView(classOverride, baseObjectView.KeyProperty, newRawObj);
            view.Parent = baseObjectView;
            return view;
        }

        // create copy of existing object side-by-side, the new object is configured to point at the base of the source object as base and have exactly the same properties as the source object
        public DataObject Action_CopyObject(DataObject parentObject, DataObject sourceObject)
        {
            // we can't copy nothing
            if (sourceObject == null)
                return null;

            // create new object exactly at the same file as the base
            var parentId = (parentObject != null) ? parentObject.ID : Guid.Empty;
            DataObject obj = new DataObject(sourceObject.File, Guid.NewGuid(), sourceObject.BaseID, parentId, sourceObject.ClassName);
            sourceObject.File.ObjectsInFile.Add(obj);

            // copy the properties
            foreach (var sourceProp in sourceObject.Properties)
            {
                // create copy of the property
                var objProp = new DataProperty(obj, sourceProp.Name, sourceProp.IsFinal, sourceProp.ClearAllIncomingValues);
                obj.Properties.Add(objProp);

                // copy the values
                foreach (var sourceValue in sourceProp.Values)
                {
                    // Note: the GUIDs are copied, it's legal since the IDs must be unique only in different object strands
                    var objValue = new DataValue(objProp, sourceValue.ID, sourceValue.Value);
                    objProp.Values.Add(objValue);
                }
            }

            // register in global object map
            AllObjects.Add(obj);
            _objectMap[obj.ID] = obj;

            // all new object are considered dirty until saved
            obj.MarkModified();
            return obj;
        }

        // create copy of existing object VIEW side-by-side, the new object is configured to point at the base of the source object as base and have exactly the same properties as the source object
        public DataObjectView Action_CopyObject(DataObjectView parentObjectView, DataObjectView sourceObjectView)
        {
            // we can't derive from nothing
            if (sourceObjectView == null)
                return null;

            // create the object
            var parentObject = (parentObjectView != null) ? parentObjectView.RawData : null;
            var newRawObj = Action_CopyObject(parentObject, sourceObjectView.RawData);

            // setup new name
            var copiedName = sourceObjectView.Name + "Copy_" + UniquePostfixGenerator();
            newRawObj.GetProperty(sourceObjectView.KeyProperty).Action_ModifyValue(copiedName);

            // setup new view
            var view = new DataObjectView(sourceObjectView.ClassName, sourceObjectView.KeyProperty, newRawObj);
            view.Parent = sourceObjectView.Parent;
            return view;
        }

        /// <summary>
        /// Can we safely delete given object ?
        /// This will validate the whole database to make sure this object is NOT used.
        /// </summary>
        /// <param name="obj">Object to delete</param>
        /// <param name="errorMessage">Optional error why can't we delete the object</param>
        /// <returns></returns>
        public bool CanDeleteObject(DataObject obj, ref string errorMessage)
        {
            // no object, can be "deleted"
            if (obj == null)
                return true;

            // the object ID as string (for direct value comparisons)
            var objIdAsString = Utils.FormatGuid(obj.ID);

            // make sure object is not a BASE of anything
            bool canDelete = true;
            foreach (var existingObj in AllObjects)
            {
                if (existingObj.BaseID == obj.ID)
                {
                    errorMessage += "Object is a base for another object (" + Utils.FormatGuid(existingObj.ID) + ")\r\n";
                    canDelete = false;
                }

                /*if (existingObj.ParentID == obj.ID)
                {
                    errorMessage += "Object is a parent for another object (" + Utils.FormatGuid(existingObj.ID) + ")\r\n";
                    canDelete = false;
                }*/

                foreach (var prop in existingObj.Properties)
                {
                    foreach (var val in prop.Values)
                    {
                        if (val.Value == objIdAsString)
                        {
                            errorMessage += "Object is linked via property '" + prop.Name + "' in object (" + Utils.FormatGuid(existingObj.ID) + ")\r\n";
                            canDelete = false;
                        }
                    }
                }
            }

            // final, merged flag
            return canDelete;
        }

        /// <summary>
        /// Remove object from data set
        /// Will validate if object CAN be deleted
        /// </summary>
        /// <param name="obj">Object to delete</param>
        /// <param name="errorMessage">Optional error message</param>
        /// <returns></returns>
        public bool Action_DeleteObject(DataObject obj, ref string errorMessage)
        {
            // Can we delete the object ?
            if (!CanDeleteObject(obj, ref errorMessage))
                return false;

            // reset all parent links (this invalidates all chains)
            foreach (var existingObj in AllObjects)
            {
                if (existingObj.ParentID == obj.ID)
                    existingObj.UnlinkFromParent();
            }

            // Remove object from file list
            obj.File.MarkModified();
            obj.File.ObjectsInFile.Remove(obj);

            // Remove the object itself
            AllObjects.Remove(obj);
            obj = null;

            // Object removed
            return true;
        }

        // extract a raw list of unassigned named objects
        // TODO: this requires HEAVY optimization
        public List<DataObjectView> ExtractNamedUnassignedObjectList(Raw.ReflectionData reflection)
        {
            // for each object check what class is it
            var outputList = new List<DataObjectView>();
            foreach (var obj in _unassignedObjects.ObjectsInFile)
            {
                if (obj.ParentID == Guid.Empty)
                {
                    // lookup the class
                    var objectClass = Utils.GetObjectClass(reflection, obj);
                    if (objectClass != null && objectClass.KeyName != "")
                    {
                        DataObjectView view = new DataObjectView( objectClass.Name, objectClass.KeyName, obj);
                        outputList.Add(view);
                    }
                }
            }

            return outputList;
        }

        // extract a raw list of named objects under given class (note: class is checked by string)
        // TODO: this requires HEAVY optimization
        public List<DataObjectView> ExtractNamedObjectList(string rootClassName, string keyPropertyName, Raw.ReflectionData reflection)
        {
            // resolve base objects for faster lookups
            foreach (var obj in AllObjects)
                obj._TempshitBaseObject = Find(obj.BaseID);

            // root class
            var rootClass = reflection.FindClass(rootClassName);

            // for each object check what class is it
            var outputList = new List<DataObjectView>();
            foreach (var obj in AllObjects)
            {
                string className = obj.ClassName;

                // if class name is not defined, get it from the base object
                {
                    var baseObj = obj._TempshitBaseObject;
                    while ((className == "") && (baseObj != null))
                    {
                        className = baseObj.ClassName;
                        baseObj = baseObj._TempshitBaseObject;
                    }
                }

                // lookup the class
                var objectClass = reflection.FindClass(className);
                if (objectClass == null || !objectClass.IsA(rootClass))
                    continue;

                // create view
                DataObjectView view = new DataObjectView(className, keyPropertyName, obj);
                outputList.Add(view);
            }

            return outputList;
        }

        // extract a list (a tree actually) of named objects under given class (note: class is checked by string)
        // TODO: this requires HEAVY optimization
        public List<DataObjectView> ExtractNamedObjects(string rootClassName, string keyPropertyName, Raw.ReflectionData reflection)
        {
            // resolve base objects for faster lookups
            foreach (var obj in AllObjects)
                obj._TempshitBaseObject = Find(obj.BaseID);

            // root class
            var rootClass = reflection.FindClass(rootClassName);

            // create helper structure to bind the objects into hierarchy based on the data dependency
            Dictionary<DataObject, DataObjectView> hierarchyMap = new Dictionary<DataObject, DataObjectView>();

            // for each object check what class is it
            foreach (var obj in AllObjects)
            {
                string className = obj.ClassName;

                // if class name is not defined, get it from the base object
                {
                    var baseObj = obj._TempshitBaseObject;
                    while ((className == "") && (baseObj != null))
                    {
                        className = baseObj.ClassName;
                        baseObj = baseObj._TempshitBaseObject;
                    }
                }

                // lookup the class
                var objectClass = reflection.FindClass(className);
                if (objectClass == null || !objectClass.IsA(rootClass))
                    continue;

                // create view
                DataObjectView view = new DataObjectView(className, keyPropertyName, obj);
                hierarchyMap[obj] = view;
            }

            // linkup parents in the hierarchy
            foreach (var view in hierarchyMap.Values)
            {
                try
                {
                    view.Parent = hierarchyMap[view.RawData._TempshitBaseObject];
                }
                catch (Exception) { }
            }

            // extract the root views
            var rootViews = new List<DataObjectView>();
            foreach (var view in hierarchyMap.Values)
                if (view.Parent == null)
                    rootViews.Add(view);

            // sort views by name
            rootViews.OrderBy(o => o.Name);

            // reset pointers to base objects
            foreach (var obj in AllObjects)
                obj._TempshitBaseObject = null;

            return rootViews;
        }

    }
}
